﻿using ATM.Components.Mock;
using ATM.Model;
using System.Diagnostics.Contracts;

namespace ATM.Components
{
    /// <summary>
    /// Main controller class, that provides mechanisms for interaction between UI and ATM.
    /// </summary>
    class AtmEngine
    {
        public AtmState atmState { get; private set; }

        public Transaction currentTransaction { get; private set; }

        public CreditCardReader creditCardReader { get; private set; }

        public Printer printer { get; private set; }

        public IBank bank { get; private set; }

        public CashDispenser cashDispenser { get; private set; }

        internal AtmEngine()
        {
            creditCardReader = new CreditCardReader();
            printer = new Printer();
            bank = new BankMock();
            cashDispenser = new CashDispenser();
        }

        /// <summary>
        /// Start a new transaction for a customer.
        /// May not start a new transaction if connection with bank is not valid.
        /// Previous transaction must be ended before new one is started.
        /// </summary>
        /// <returns>true if transaction was started</returns>
        internal bool StartTransacation()
        {
            Contract.Requires(atmState == AtmState.IDLE);
            Contract.Requires(currentTransaction == null);
            Contract.Requires(bank != null);

            // Let's verify connection with bank. We cannot do anything without them :)
            if (bank.VerifyConnection())
            {
                currentTransaction = new Transaction();
                atmState = AtmState.WAITING_FOR_CREDIT_CARD;
                return true;
            }
            else
            {
                atmState = AtmState.TRANSACTION_INVALID;
                return false;
            }
        }

        /// <summary>
        /// Reads a credit card that customer put in the reader. 
        /// </summary>
        /// <returns>true if credit card was read successfully</returns>
        internal bool ReadCreditCard()
        {
            Contract.Requires(atmState == AtmState.WAITING_FOR_CREDIT_CARD);
            Contract.Requires(creditCardReader != null);

            // ATM will try to read the card maximum 3 times before it aborts
            CreditCardInfo tempCreditCardInfo = creditCardReader.ReadCreditCard(3);
            if (tempCreditCardInfo != null)
            {
                atmState = AtmState.CREDIT_CARD_READ;
                currentTransaction.CreditCardInfo = tempCreditCardInfo;
                return true;
            }
            else
            {
                atmState = AtmState.TRANSACTION_INVALID;
                return false;
            }
        }

        /// <summary>
        /// Checks the printer status.
        /// </summary>
        /// <returns>true if printer is operating</returns>
        internal bool CheckPrinter()
        {
            Contract.Requires(atmState == AtmState.CREDIT_CARD_READ);
            Contract.Requires(currentTransaction != null);
            Contract.Requires(printer != null);

            bool printerCheckResult = printer.CheckPaper();

            atmState = AtmState.PRINTER_CHECKED;

            return printerCheckResult;
        }

        /// <summary>
        /// Verifies the credit card with a bank and returns a status.
        /// </summary>
        /// <returns>reutrns OK if card was verified successfully, INVALID if card is corrupted or BLOCKED in case card was blocked</returns>
        internal CreditCardVerificationStatus VerifyCreditCard()
        {
            Contract.Requires(atmState == AtmState.PRINTER_CHECKED);
            Contract.Requires(currentTransaction != null);
            Contract.Requires(currentTransaction.CreditCardInfo != null);
            Contract.Requires(currentTransaction.PinCode == null);
            Contract.Requires(currentTransaction.Amount == null);
            Contract.Requires(bank != null);


            CreditCardVerificationStatus status = bank.VerifyCreditCard(currentTransaction.CreditCardInfo);
            if (status == CreditCardVerificationStatus.OK)
            {
                atmState = AtmState.CARD_VERIFIED;
            }
            else if (status == CreditCardVerificationStatus.INVALID)
            {
                atmState = AtmState.TRANSACTION_INVALID;
            }
            else if (status == CreditCardVerificationStatus.BLOCKED)
            {
                atmState = AtmState.TRANSACTION_INVALID;
                // Credit card is blocked we need to print confirmation
                if (printer.CheckPaper()) printer.PrintCardWithheldConfirmation();

            }
            return status;
        }

        /// <summary>
        /// Verifies the PIN code for card in current transaction.
        /// </summary>
        /// <param name="pinCode">PIN code to verify</param>
        /// <returns>OK if PIN was correct or FAILED_CARD_LOCKED if wrong PIN was entered too many times</returns>
        internal PinCodeVerificationStatus VerifyPinCode(string pinCode)
        {
            Contract.Requires(atmState == AtmState.CARD_VERIFIED);
            Contract.Requires(currentTransaction != null);
            Contract.Requires(currentTransaction.CreditCardInfo != null);
            Contract.Requires(currentTransaction.PinCode == null);
            Contract.Requires(currentTransaction.Amount == null);
            Contract.Requires(pinCode != null);
            Contract.Requires(bank != null);

            PinCodeVerificationStatus status = bank.VerifyPinCode(currentTransaction.CreditCardInfo.Number, pinCode);
            if (status == PinCodeVerificationStatus.OK)
            {
                atmState = AtmState.PIN_VERIFIED;
                currentTransaction.PinCode = pinCode;
            }
            else if (status == PinCodeVerificationStatus.FAILED_CARD_LOCKED)
            {
                atmState = AtmState.TRANSACTION_INVALID;
                // Credit card is blocked we need to print confirmation
                if (printer.CheckPaper()) printer.PrintCardWithheldConfirmation();
            }
            return status;
        }

        /// <summary>
        /// Withdraws given amount from credit card in current transaction.
        /// </summary>
        /// <param name="amount">Amount to withdraw</param>
        /// <returns>OK if cash can be withdrawn, FAILED_NOT_ENOUGH_FUNDS if there was not enough funds on credit card or FAILED_NOT_ENOUGH_CASH_IN_ATM if ATM does not have enough funds</returns>
        internal WithdrawStatus Withdraw(int amount)
        {
            Contract.Requires(atmState == AtmState.PIN_VERIFIED);
            Contract.Requires(currentTransaction != null);
            Contract.Requires(currentTransaction.CreditCardInfo != null);
            Contract.Requires(currentTransaction.PinCode != null);
            Contract.Requires(currentTransaction.Amount == null);
            Contract.Requires(amount > 0);
            Contract.Requires(cashDispenser != null);
            Contract.Requires(cashDispenser.Db != null);
            Contract.Requires(cashDispenser.Db.trays != null);
            Contract.Requires(cashDispenser.Db.trays.Items != null);
            Contract.Requires(cashDispenser.Db.trays.Items.Length > 0);

            WithdrawStatus status;

            // First let's check if ATM has enough cash
            if (cashDispenser.HasEnoughCash(amount))
            {
                // If we have cash in ATM, lets withdraw from bank
                bool withdrawStatus = bank.WithdrawAmount(currentTransaction.CreditCardInfo.Number, currentTransaction.PinCode, amount);
                // So we have withdrawn it from bank, let's give it the the client
                if (withdrawStatus)
                {
                    cashDispenser.ejectAmount(amount);

                    // Credit card is blocked we need to print confirmation
                    if (printer.CheckPaper()) printer.PrintTransactionConfirmation(amount);

                    status = WithdrawStatus.OK;
                }
                else
                {
                    // I know that feeling bro...
                    status = WithdrawStatus.FAILED_NOT_ENOUGH_FUNDS;
                }
            }
            else
            {
                // Well... people are rich nowadays? Maybe he will be also happy with smaller amount?
                status = WithdrawStatus.FAILED_NOT_ENOUGH_CASH_IN_ATM;
            }

            return status;
        }

        /// <summary>
        /// Ends current transaction.
        /// </summary>
        internal void EndTransaction()
        {
            Contract.Requires(atmState != AtmState.IDLE);
            Contract.Requires(currentTransaction != null);

            currentTransaction = null;
            atmState = AtmState.IDLE;
        }

    }
}
